#!/bin/sh

## Updater für die user.js von Privacy-Handbuch.de
## Version: 1.3.0
## by: TotallyLeGIT

## based on the ghacks user.js updater by:
## Author: Pat Johnson (@overdodactyl)
## Additional contributors: @earthlng, @ema-pe, @claustromaniac

## Dependencies: ·curl or wget

scriptLocation="$(readlink -f "${0}" 2>/dev/null || greadlink -f "${0}" 2>/dev/null || printf "%s" "${0}")"
readonly scriptLocation

#########################
#    Base variables     #
#########################

# Colors used for printing only when stdout is a terminal
if [ -t 1 ]; then
    red="$(tput setaf 1)$(tput bold)"
    blue="$(tput setaf 4)"
    green="$(tput setaf 2)"
    yellow="$(tput setaf 3)"
    neutral="$(tput sgr0)"   # No Color
else
    red=""
    blue=""
    green=""
    yellow=""
    neutral=""
fi

# Argument defaults
BACKUP='multiple'
COMPARE=false
configWanted='keep'
CONFIRM='yes'
esr=0
OVERRIDE="soft" # soft: look for user-overrides.js but only give info when none found
overridePath='user-overrides.js'
profilePathOption=false
VIEW=false
UPDATE='no'
readonly backupsDir="userjs_backups"
readonly diffsDir="userjs_diffs"

# Download method priority: curl → wget
if command -v curl >/dev/null; then
  DOWNLOAD_COMMAND='curl --max-redirs 2 --max-time 30 --fail --silent --output'
elif command -v wget >/dev/null; then
  # -O file = --output-document=file
  DOWNLOAD_COMMAND='wget --max-redirect=2 --timeout=30 --quiet -O'
else
  echo "${red}This script requires curl or wget."
  echo "Process aborted.${neutral}"
  exit 2
fi

show_banner () {
  echo "${blue}
    #########################################################################
    ####              Updater for Privacy-Handbuch's user.js             ####
    ####            Suitable for both Firefox and Thunderbird            ####
    #########################################################################${neutral}

Updater:             ${blue}https://notabug.org/TotallyLeGIT/PH-userjs-updater${neutral}
Firefox user.js:     ${blue}https://www.privacy-handbuch.de/handbuch_21u.htm${neutral}
Thunderbird user.js: ${blue}https://www.privacy-handbuch.de/handbuch_31p.htm${neutral}
"
}


#########################
#      Arguments        #
#########################

usage() {
show_banner
  echo
  echo "${blue}Usage: ${0} [-bdhilnrsuy] [-c CONFIG] [-e VERSION] [-o OVERRIDE] [-p PROFILE]${neutral}" 1>&2   # Echo usage string to standard error
  echo "
    -b           Only keep one user.js backup and one diff file.
    -c CONFIG    Specify the Firefox user.js config you want: minimal, moderat, streng or hotspot
                 or for Thunderbird: tb or tb2 .
                 This is only needed if you want to change to another config.
    -d           Create a diff file comparing old and new user.js within ${diffsDir} subdirectory.
    -e VERSION   Force download of user.js for Firefox ESR with version number VERSION.
                 Example: \"-e 91\" will download the user.js for Firefox ESR Version 91.x .
    -h           Show this help message and exit.
    -i           Inspect the resulting user.js file.
    -l           Interactively choose your profile from a list.
    -n           Do not append any overrides even if user-overrides.js exists.
    -o OVERRIDE  Filename or path to overrides file, needed if different than PROFILE/user-overrides.js .
                 If used with -p, the path may be relative to PROFILE or an absolute path.
                 If given a directory, all files inside ending in .js will be appended.
    -p PROFILE   Path to your profile directory, needed if different than this script's location.
                 May be used multiple times.
                 IMPORTANT: If the profile path includes spaces, wrap whole path in quotes.
    -r           Only download user.js to a temporary file and open it. This requires -c.
    -s           Check for updates of this updater and update silently without confirmation.
    -u           Check for updates of this updater and ask for confirmation if update available.
    -y           Update user.js without confirmation.
"
}


#########################
#  (Pro)File Handling   #
#########################

abstract_OS () {
  if [ "$(uname)" = 'Darwin' ]; then
    readonly open_command='open'
    readonly version_command='/Applications/Firefox.app/Contents/MacOS/firefox --version'
    readonly ff_file_path="${HOME}/Library/Application Support/Firefox/profiles.ini"
    readonly tb_file_path="${HOME}/Library/Thunderbird/profiles.ini"
  else
    readonly open_command='xdg-open'
    readonly version_command='firefox --version'
    readonly ff_file_path="${HOME}/.mozilla/firefox/profiles.ini"
    readonly tb_file_path="${HOME}/.thunderbird/profiles.ini"
  fi
  # set effective path for each profile.ini
  [ -f "${ff_file_path}" ] && fffile="${ff_file_path}" || fffile=""
  [ -f "${tb_file_path}" ] && tbfile="${tb_file_path}" || tbfile=""
  readonly fffile tbfile
}

# downloads a resource. expects two arguments:
# $1 the variable name, where the filename will be saved to
# $2 the resource to download
# example usage: download_file 'var' 'https://example.com/resource'
download_file () {
  eval "$1=$(mktemp)"
  if ! eval "${DOWNLOAD_COMMAND} \$${1} ${2}"; then
    echo "${red}Download of resource failed:${neutral} ${2}"
    return 33
  fi
}

open_file () {    # expects one argument: file_path
  eval "${open_command} ${1}" || echo "${red}Error: Sorry, opening" \
    "files is not supported for your OS.${neutral}"
}

readIniFile () {   # expects one argument: absolute path of profiles.ini
  readonly inifile="${1}"

  if [ "$(grep -c '^\[Profile' "${inifile}")" -eq 1 ]; then   # only 1 profile found
    tempIni="$(grep '^\[Profile' -A 4 "${inifile}")"
  else   # more than 1 profile
    echo 'Profiles found:'
    echo '–––––––––––––––––––––––––––––––––––'
    grep --color=never -E 'Default=[^1]|\[Profile[0-9]*\]|Name=|Path=|^$' "${inifile}"
    echo '–––––––––––––––––––––––––––––––––––'
    echo 'Select the profile number ( 0 for Profile0, 1 for Profile1 etc. ):'
    read -r reply
    echo
    tempIni="$(grep '^\[Profile'"${reply}" -A 4 "${inifile}")" || { echo "${red}Profile${reply} does not exist!${neutral}" && exit 1 ; }
  fi

  tempProfilePath="$(echo "${tempIni}" | sed -n 's/^Path=\(.*\)$/\1/p')"
  pathIsRel="$(echo "${tempIni}" | sed -n 's/^IsRelative=\([01]\)$/\1/p')"
  readonly pathIsRel

  # update global variable if path is relative
  [ "${pathIsRel}" -eq 1 ] && tempProfilePath="$(dirname "${inifile}")/${tempProfilePath}"
  updateProfilePath "${tempProfilePath}"
}

getProfilePath () {
  if [ "${profilePathOption}" = 'list' ]; then
    if [ -n "${fffile}" ]; then
      ini="${fffile}"
      [ -n "${tbfile}" ] && ini="both" || printf "Firefox detected.\n\n"
    elif [ -n "${tbfile}" ]; then
      ini="${tbfile}"
      printf "Thunderbird detected.\n\n"
    else
      echo "${red}Error: Sorry, Thunderbird and Firefox could not be found or -l is not supported for your OS. Try -p option.${neutral}" && exit 1
    fi

    if [ "${ini}" = "both" ]; then
      echo "(F)irefox and (T)hunderbird were detected. Select one: F/T"
      read -r reply
      reply="$(printf "%s" "${reply}" | tr '[:upper:]' '[:lower:]')"
      { [ -z "${reply##f*}" ] && ini="${fffile}" && printf "Firefox was selected.\n\n" ; } || { [ -z "${reply##t*}" ] && ini="${tbfile}" && printf "Thunderbird was selected.\n\n" ; } || { echo "${red}Error: Sorry, could not parse your input.${neutral}" && exit 1 ; }
    fi
    readIniFile "${ini}"   # updates PROFILE_PATH or exits on error
  elif [ "${profilePathOption}" = false ]; then
    PROFILE_PATH="$(dirname "${scriptLocation}")"
  fi
}

updateProfilePath () {   # expects a profile path as argument
  # if profile path empty: update it
  if [ -z "${PROFILE_PATH}" ]; then
    PROFILE_PATH="${1}"
    # update profilePathOption var so we know there's a profile path
    [ "${profilePathOption}" = "false" ] && profilePathOption="true"
  else   # append additional profile path
    PROFILE_PATH="${PROFILE_PATH}\n${1}"
  fi
}


#########################
#    Update updater     #
#########################

# Returns the version number of the updater file in the first argument.
get_updater_version () {
  sed -n '4 s/.*[[:blank:]]\([0-9]*\.[0-9]*\.[0-9]*\)[[:graph:]]*/\1/p' "${1}"
}

# Default: Do not check for updates of the updater at all.
# Args:
#   -u: Check for update. if available, ask user if they want to update.
#   -s: Check for update, if available, execute without asking.
update_updater () {
  if [ "${UPDATE}" = 'no' ]; then
    return 0   # Do not to check for updates.
  fi
  download_file 'tmp_updater' \
    'https://notabug.org/TotallyLeGIT/PH-userjs-updater/raw/main/updater' \
    || return 1
  readonly tmp_updater
  updaterVerOld="$(get_updater_version "${scriptLocation}")"
  updaterVerNew="$(get_updater_version "${tmp_updater}")"
  readonly updaterVerOldConst="${updaterVerOld}"

  # compare updater versions, if different
  while [ ! "${updaterVerOld}" = "${updaterVerNew}" ]; do
    difference="$((${updaterVerNew%%.*}-${updaterVerOld%%.*}))"
    if [ "${difference}" -gt 0 ]; then  # newer version available
      if [ "${UPDATE}" = 'yes' ]; then  # ask to apply update
        echo "There is a newer version of the updater available. ${red}Update? Y/N${neutral}"
        read -r reply
        echo "${reply}" | grep -qv "^[yY]*$" && return 0   # Update available, but user chooses not to update
      fi
      # apply update, regardless if silent or after "yes"
      mv "${tmp_updater}" "${scriptLocation}"
      chmod u+x "${scriptLocation}"
      echo "Updater successfully updated."
      # show changes for this version to the user
      if download_file 'tmp_changelog' \
        'https://notabug.org/TotallyLeGIT/PH-userjs-updater/raw/main/CHANGELOG.md'
      then
        readonly tmp_changelog
        echo "${green}Changelog:${neutral}"
        echo
        sed -ne '/^## '"${updaterVerOldConst}"'$/ !p' -e "/${updaterVerOldConst}/ q" "${tmp_changelog}"
      fi
      break
    elif [ "${difference}" -eq 0 ]; then  # version number is equal (go to next number)
      updaterVerOld="${updaterVerOld#*.}"
      updaterVerNew="${updaterVerNew#*.}"
    else  # this should not be happening
      echo "${red}Error: Something with the version numbers seems funny. Check updater homepage and/or update it manually.${neutral}"
      return 2
    fi
  done
return 0
}


#########################
#    Update user.js     #
#########################

# returns a user.js download link. expects the current version as argument. e.g.: streng_16022020
get_userjs_link () {
  configIs="${1%_*}"
  configIs="${configIs#*_}"
  [ "${configWanted}" = "keep" ] && configWanted="${configIs}" && [ "${configIs}" = "Not detected." ] && exit 33
  if [ "${configIs%%2*}" != "tb" ]; then
    [ "${esr}" = "0" ] && esrurl="" || esrurl="ff${esr}/"
  fi

  echo "https://www.privacy-handbuch.de/download/${esrurl}${configWanted}/user.js"
}

# Returns version number of a user.js file
get_userjs_version () {
  [ -e "${1}" ] && returnVer="$(sed -ne '/user\.js\.prhdb/ s/.*"\(\([[:alnum:]]*_\)*[[:alpha:]]*_[[:digit:]]*\)".*/\1/p' -e '10 q' "${1}")"
  #[ -e "${1}" ] && returnVer="$(sed -n '5 s/.*"\([[:alnum:]_]\+[0-9]\+\)".*/\1/p' "${1}")"
  [ -z "${returnVer}" ] && echo "Not detected." && exit 33
  echo "${returnVer}"
}

# returns 0 if firefox is release or the major version number if firefox is esr
is_esr () {
  # result is e. g.: 85.0 or 78.7
  ffver="$(eval "${version_command}" 2>/dev/null | cut -d ' ' -f 3 \
    | cut -d '.' -f -2)" || return 1
  [ "${ffver##*.}" -eq 0 ] && echo "0" || echo "${ffver%%.*}"
}

add_override () {
  input="${1}"

  if [ -f "${input}" ]; then
    echo >> user.js
    cat "${input}" >> user.js
    echo "Status: ${green}Override file appended:${neutral} ${input}"
  elif [ -d "${input}" ]; then
    find "${input}" -mindepth 1 -maxdepth 1 -name "*.js" |
    while IFS= read -r f
    do
      add_override "${f}"
    done
  else
    if [ "${OVERRIDE}" = "soft" ]; then
      echo "Info: No override file or directory applied."
    else
      echo "${yellow}Warning: Could not find override file or directory:${neutral} ${input}"
    fi
  fi
}

# Applies latest version of user.js and any custom overrides
update_userjs () {
  currentVerFull="$(get_userjs_version user.js)"
  # if esr wasn't forced, try to detect it
  if [ "${esr}" -eq "0" ]; then
    if esr="$(is_esr)"; then
      [ "${esr}" -eq "0" ] && esrstring="Release" || esrstring="ESR ${esr}"
    else
      esrstring="not found" # likely because 'firefox --version' failed
    fi
  fi

  echo "Firefox version:  ${green}${esrstring}${neutral}"
  echo "Profile path:     ${green}${PWD}${neutral}"
  echo "Currently using:  ${green}${currentVerFull}${neutral}"

  # some error handling while setting variables and telling where it went wrong
  link="$(get_userjs_link "${currentVerFull}")"
  [ "${?}" -eq 33 ] && echo "${red}Error: Could not determine which config to download. Use -c CONFIG and/or -p PROFILE. ${neutral}" && exit 1
  download_file 'tmp_userjs' "${link}" \
    || exit 1
  readonly tmp_userjs
  newVersion="$(get_userjs_version "${tmp_userjs}")"
  [ "${?}" -eq 33 ] && echo "${red}Error parsing new user.js${neutral}" && exit 1
  echo "Available online: ${green}${newVersion}${neutral}"
  echo
  if [ "${CONFIRM}" = 'yes' ]; then
    echo "This script will update to the latest user.js file and append any custom configurations from user-overrides.js. ${red}Continue? Y/N ${neutral}"
    read -r reply </dev/tty
    echo
    echo "${reply}" | grep -qv "^[yY]*$" && echo "${red}Process aborted.${neutral}" && rm "${tmp_userjs}" && return 1
  fi

  # backup user.js
  mkdir -p "${backupsDir}"
  [ "${BACKUP}" = 'single' ] && bakDate="$(date -Iseconds)." || bakDate=""
  bakName="${backupsDir}/${bakDate}user.js"
  [ -f user.js ] && mv user.js "${bakName}"

  # install newest user.js
  mv "${tmp_userjs}" user.js
  echo "Status: ${green}user.js has been backed up and replaced with the latest version!${neutral}"

  # apply overrides
  if [ "${OVERRIDE}" != "false" ]; then
    printf "\n%s\n" "// user-overrides" >> user.js
    add_override "${overridePath}"
  fi

  # create diff
  if [ "${COMPARE}" = true ]; then
    [ "${BACKUP}" = 'single' ] && diffDate="$(date -Iseconds)." || diffDate=""
    diffName="${diffsDir}/${diffDate}diff.txt"
    diffs="$(diff -wBU 0 "${bakName}" user.js)"

    if [ -n "${diffs}" ]; then
      mkdir -p "${diffsDir}"
      echo "${diffs}" > "${diffName}"
      echo "Status: ${green}A diff file was created:${neutral} ${PWD}/${diffName}"
    else
      echo "${yellow}Your new user.js file appears to be identical. No diff file was created.${neutral}"
    fi
  fi
  [ "${VIEW}" = true ] && open_file "${PWD}/user.js"
}


#########################
#        Execute        #
#########################

abstract_OS
if [ ${#} != 0 ]; then
  firstArg="$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]')"
  # Display usage if first argument contains -help
  if [ -z "${firstArg##*-help*}" ]; then
    usage
    exit 0
  else
    while getopts ":bc:de:hilno:p:rsuy" opt; do
      case "${opt}" in
        b)
          BACKUP='single'
          ;;
        c)
          if ! configWanted="$(printf "%s" "${OPTARG}" | tr '[:upper:]' '[:lower:]' | grep -om 1 '^minimal\|^moderat\|^streng\|^hotspot\|^tb\|^tb2')"; then
            echo "${red}Error: Invalid CONFIG.${neutral} Valid values are: minimal, moderat, streng, hotspot, tb or tb2"
            exit 1
          fi
          ;;
        d)
          COMPARE=true
          ;;
        e)
          esr="${OPTARG}"
          ;;
        h)
          usage
          exit 0
          ;;
        i)
          VIEW=true
          ;;
        l)
          profilePathOption='list'
          ;;
        n)
          OVERRIDE="false"
          ;;
        o)
          OVERRIDE="true"
          overridePath="${OPTARG}"
          ;;
        p)
          updateProfilePath "${OPTARG}"
          ;;
        r)
          if [ "${configWanted}" = "keep" ]; then
            echo "${red}Error: -r option requires -c option, too.${neutral}"
            exit 1
          else
            download_file 'tmp_read_userjs' \
              "$(get_userjs_link "${configWanted}")" \
              || exit 1
            readonly tmp_read_userjs
          fi
          echo "${green}user.js was saved to temporary file ${tmp_read_userjs}${neutral}"
          open_file "${tmp_read_userjs}"
          exit 0
          ;;
        s)
          UPDATE='silent'
          ;;
        u)
          UPDATE='yes'
          ;;
        y)
          CONFIRM='no'
          ;;
        \?)
          echo
          echo "${red}Error! Invalid option: -${OPTARG}${neutral}" 1>&2
          usage
          exit 2
          ;;
        :)
          echo "${red}Error! Option -${OPTARG} requires an argument.${neutral}" 1>&2
          exit 2
          ;;
      esac
    done
  fi
fi

getProfilePath   # updates PROFILE_PATH or exits on error
echo "${PROFILE_PATH}" |
while read -r path; do
  cd "${path}" || { echo "${red}Error: No profile path found.${neutral}" && exit 1 ; }
  update_userjs
  echo
done
update_updater
